package com.homihq.db2rest.jdbc.sql;

import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.NClob;
import java.sql.Ref;
import java.sql.RowId;
import java.sql.SQLXML;
import java.sql.Struct;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
public class JdbcTypeJavaClassMappings {

    public static final JdbcTypeJavaClassMappings INSTANCE = new JdbcTypeJavaClassMappings();

    private final ConcurrentHashMap<Class<?>, Integer> javaClassToJdbcTypeCodeMap;
    private final ConcurrentHashMap<Integer, Class<?>> jdbcTypeCodeToJavaClassMap;

    private JdbcTypeJavaClassMappings() {
        javaClassToJdbcTypeCodeMap = buildJavaClassToJdbcTypeCodeMappings();
        jdbcTypeCodeToJavaClassMap = buildJdbcTypeCodeToJavaClassMappings();
    }

    private static ConcurrentHashMap<Class<?>, Integer> buildJavaClassToJdbcTypeCodeMappings() {
        final ConcurrentHashMap<Class<?>, Integer> workMap = new ConcurrentHashMap<>();

        // these mappings are the ones outlined specifically in the spec
        workMap.put(String.class, SqlTypes.VARCHAR);
        workMap.put(BigDecimal.class, SqlTypes.NUMERIC);
        workMap.put(BigInteger.class, SqlTypes.NUMERIC);
        workMap.put(Boolean.class, SqlTypes.BIT);
        workMap.put(Byte.class, SqlTypes.TINYINT);
        workMap.put(Short.class, SqlTypes.SMALLINT);
        workMap.put(Integer.class, SqlTypes.INTEGER);
        workMap.put(Long.class, SqlTypes.BIGINT);
        workMap.put(Float.class, SqlTypes.REAL);
        workMap.put(Double.class, SqlTypes.DOUBLE);
        workMap.put(byte[].class, SqlTypes.VARBINARY);
        workMap.put(java.sql.Date.class, SqlTypes.DATE);
        workMap.put(Time.class, SqlTypes.TIME);
        workMap.put(Timestamp.class, SqlTypes.TIMESTAMP);
        workMap.put(LocalTime.class, SqlTypes.TIME);
        workMap.put(OffsetTime.class, SqlTypes.TIME_WITH_TIMEZONE);
        workMap.put(LocalDate.class, SqlTypes.DATE);
        workMap.put(LocalDateTime.class, SqlTypes.TIMESTAMP);
        workMap.put(OffsetDateTime.class, SqlTypes.TIMESTAMP_WITH_TIMEZONE);
        workMap.put(ZonedDateTime.class, SqlTypes.TIMESTAMP_WITH_TIMEZONE);
        workMap.put(Instant.class, SqlTypes.TIMESTAMP_UTC);
        workMap.put(Blob.class, SqlTypes.BLOB);
        workMap.put(Clob.class, SqlTypes.CLOB);
        //workMap.put( Array.class, SqlTypes.ARRAY );
        workMap.put(Struct.class, SqlTypes.STRUCT);
        workMap.put(Ref.class, SqlTypes.REF);
        workMap.put(Class.class, SqlTypes.JAVA_OBJECT);
        workMap.put(RowId.class, SqlTypes.ROWID);
        workMap.put(SQLXML.class, SqlTypes.SQLXML);
        workMap.put(UUID.class, SqlTypes.UUID);
        workMap.put(InetAddress.class, SqlTypes.INET);
        workMap.put(Inet4Address.class, SqlTypes.INET);
        workMap.put(Inet6Address.class, SqlTypes.INET);
        workMap.put(Duration.class, SqlTypes.INTERVAL_SECOND);


        // additional "common sense" registrations
        workMap.put(Character.class, SqlTypes.CHAR);
        workMap.put(char[].class, SqlTypes.VARCHAR);
        //		workMap.put( Character[].class, SqlTypes.VARCHAR );
        //		workMap.put( Byte[].class, SqlTypes.VARBINARY );
        workMap.put(java.util.Date.class, SqlTypes.TIMESTAMP);
        workMap.put(Calendar.class, SqlTypes.TIMESTAMP);

        return workMap;
    }

    private static ConcurrentHashMap<Integer, Class<?>> buildJdbcTypeCodeToJavaClassMappings() {
        final ConcurrentHashMap<Integer, Class<?>> workMap = new ConcurrentHashMap<>();

        workMap.put(SqlTypes.CHAR, String.class);
        workMap.put(SqlTypes.VARCHAR, String.class);
        workMap.put(SqlTypes.LONGVARCHAR, String.class);
        workMap.put(SqlTypes.NCHAR, String.class);
        workMap.put(SqlTypes.NVARCHAR, String.class);
        workMap.put(SqlTypes.LONGNVARCHAR, String.class);
        workMap.put(SqlTypes.NUMERIC, BigDecimal.class);
        workMap.put(SqlTypes.DECIMAL, BigDecimal.class);
        workMap.put(SqlTypes.BIT, Boolean.class);
        workMap.put(SqlTypes.BOOLEAN, Boolean.class);
        workMap.put(SqlTypes.TINYINT, Byte.class);
        workMap.put(SqlTypes.SMALLINT, Short.class);
        workMap.put(SqlTypes.INTEGER, Integer.class);
        workMap.put(SqlTypes.BIGINT, Long.class);
        workMap.put(SqlTypes.REAL, Float.class);
        workMap.put(SqlTypes.DOUBLE, Double.class);
        workMap.put(SqlTypes.FLOAT, Double.class);
        workMap.put(SqlTypes.BINARY, byte[].class);
        workMap.put(SqlTypes.VARBINARY, byte[].class);
        workMap.put(SqlTypes.LONGVARBINARY, byte[].class);
        workMap.put(SqlTypes.DATE, java.sql.Date.class);
        workMap.put(SqlTypes.TIME, Time.class);
        workMap.put(SqlTypes.TIMESTAMP, Timestamp.class);
        workMap.put(SqlTypes.TIME_WITH_TIMEZONE, OffsetTime.class);
        workMap.put(SqlTypes.TIMESTAMP_WITH_TIMEZONE, OffsetDateTime.class);
        workMap.put(SqlTypes.BLOB, Blob.class);
        workMap.put(SqlTypes.CLOB, Clob.class);
        workMap.put(SqlTypes.NCLOB, NClob.class);
        //workMap.put( SqlTypes.ARRAY, Array.class );
        workMap.put(SqlTypes.STRUCT, Struct.class);
        workMap.put(SqlTypes.REF, Ref.class);
        workMap.put(SqlTypes.JAVA_OBJECT, Class.class);
        workMap.put(SqlTypes.ROWID, RowId.class);
        workMap.put(SqlTypes.SQLXML, SQLXML.class);
        workMap.put(SqlTypes.UUID, UUID.class);
        workMap.put(SqlTypes.JSON, String.class);
        workMap.put(SqlTypes.INET, InetAddress.class);
        workMap.put(SqlTypes.TIMESTAMP_UTC, Instant.class);
        workMap.put(SqlTypes.INTERVAL_SECOND, Duration.class);

        return workMap;
    }

    /**
     * For the given Java type, determine the JDBC recommended JDBC type.
     * <p>
     * This includes the mappings defined in <em>TABLE B-2: Java Types Mapped to JDBC Types</em>
     * and <em>TABLE B-4: Java Object Types Mapped to JDBC Types</em>, as well as some additional
     * "common sense" mappings.
     */
    public int determineJdbcTypeCodeForJavaClass(Class<?> cls) {
        Integer typeCode = javaClassToJdbcTypeCodeMap.get(cls);
        if (typeCode != null) {
            return typeCode;
        }

        int specialCode = cls.hashCode();
        log.debug(
                "JDBC type code mapping not known for class [" + cls.getName()
                        + "]; using custom code [" + specialCode + "]"
        );
        return specialCode;
    }


    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    /**
     * For the given JDBC type, determine the JDBC recommended Java type.
     * <p>
     * These mappings are defined by <em>TABLE B-1: JDBC Types Mapped to Java Types</em>.
     */
    public Class<?> determineJavaClassForJdbcTypeCode(Integer typeCode) {
        Class<?> cls = jdbcTypeCodeToJavaClassMap.get(typeCode);
        if (cls != null) {
            return cls;
        }

        log.debug(
                "Java Class mapping not known for JDBC type code {}}; using java.lang.Object",
                typeCode
        );
        return Object.class;
    }

    /**
     * @see #determineJavaClassForJdbcTypeCode(Integer)
     */
    public Class<?> determineJavaClassForJdbcTypeCode(int typeCode) {
        return determineJavaClassForJdbcTypeCode(Integer.valueOf(typeCode));
    }
}
